/**
 * juicer > A Light Javascript Templete Engine.
 * raw    > https://github.com/PaulGuo/Juicer/blob/master/src/juicer.js
 * edit   > bh-lay.com
 */

var juicer = function() {
    var args = [].slice.call(arguments);

    args.push(juicer.options);

    if(args[0].match(/^\s*#([\w:\-\.]+)\s*$/igm)) {
        args[0].replace(/^\s*#([\w:\-\.]+)\s*$/igm, function($, $id) {
            var _document = document;
            var elem = _document && _document.getElementById($id);
            args[0] = elem ? (elem.value || elem.innerHTML) : $;
        });
    }

    if(juicer.documentHTML) {
        juicer.compile.call(juicer, juicer.documentHTML);
        juicer.documentHTML = '';
    }

    if(arguments.length == 1) {
        return juicer.compile.apply(juicer, args);
    }

    if(arguments.length >= 2) {
        return juicer.to_html.apply(juicer, args);
    }
};

var __escapehtml = {
    escapehash: {
        '<': '&lt;',
        '>': '&gt;',
        '&': '&amp;',
        '"': '&quot;',
        "'": '&#x27;',
        '/': '&#x2f;'
    },
    escapereplace: function(k) {
        return __escapehtml.escapehash[k];
    },
    escaping: function(str) {
        return typeof(str) !== 'string' ? str : str.replace(/[&<>"']/igm, this.escapereplace);
    },
    detection: function(data) {
        return typeof(data) === 'undefined' ? '' : data;
    }
};

var __throw = function(error) {
    if(typeof(console) !== 'undefined') {
        if(console.warn) {
            console.warn(error);
            return;
        }

        if(console.log) {
            console.log(error);
            return;
        }
    }

    throw(error);
};

var __creator = function(o, proto) {
    o = o !== Object(o) ? {} : o;

    if(o.__proto__) {
        o.__proto__ = proto;
        return o;
    }

    var empty = function() {};
    var n = Object.create ?
        Object.create(proto) :
        new(empty.prototype = proto, empty);

    for(var i in o) {
        if(o.hasOwnProperty(i)) {
            n[i] = o[i];
        }
    }

    return n;
};

var annotate = function(fn) {
    var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
    var FN_ARG_SPLIT = /,/;
    var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
    var FN_BODY = /^function[^{]+{([\s\S]*)}/m;
    var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
    var args = [],
        fnText,
        fnBody,
        argDecl;

    if (typeof fn === 'function') {
        if (fn.length) {
            fnText = fn.toString();
        }
    } else if(typeof fn === 'string') {
        fnText = fn;
    }

    fnText = fnText.trim();
    argDecl = fnText.match(FN_ARGS);
    fnBody = fnText.match(FN_BODY)[1].trim();

    for(var i = 0; i < argDecl[1].split(FN_ARG_SPLIT).length; i++) {
        var arg = argDecl[1].split(FN_ARG_SPLIT)[i];
        arg.replace(FN_ARG, function(all, underscore, name) {
            args.push(name);
        });
    }

    return [args, fnBody];
};

juicer.__cache = {};
juicer.version = '0.6.15';
juicer.settings = {};
juicer.documentHTML = '';

juicer.tags = {
    operationOpen: '{@',
    operationClose: '}',
    interpolateOpen: '\\${',
    interpolateClose: '}',
    noneencodeOpen: '\\$\\${',
    noneencodeClose: '}',
    commentOpen: '\\{#',
    commentClose: '\\}'
};

juicer.options = {
    cache: true,
    strip: true,
    errorhandling: true,
    detection: true,
    _method: __creator({
        __escapehtml: __escapehtml,
        __throw: __throw,
        __juicer: juicer
    }, {})
};

juicer.tagInit = function() {
    var forstart = juicer.tags.operationOpen + 'each\\s*([^}]*?)\\s*as\\s*(\\w*?)\\s*(,\\s*\\w*?)?' + juicer.tags.operationClose;
    var forend = juicer.tags.operationOpen + '\\/each' + juicer.tags.operationClose;
    var ifstart = juicer.tags.operationOpen + 'if\\s*([^}]*?)' + juicer.tags.operationClose;
    var ifend = juicer.tags.operationOpen + '\\/if' + juicer.tags.operationClose;
    var elsestart = juicer.tags.operationOpen + 'else' + juicer.tags.operationClose;
    var elseifstart = juicer.tags.operationOpen + 'else if\\s*([^}]*?)' + juicer.tags.operationClose;
    var interpolate = juicer.tags.interpolateOpen + '([\\s\\S]+?)' + juicer.tags.interpolateClose;
    var noneencode = juicer.tags.noneencodeOpen + '([\\s\\S]+?)' + juicer.tags.noneencodeClose;
    var inlinecomment = juicer.tags.commentOpen + '[^}]*?' + juicer.tags.commentClose;
    var rangestart = juicer.tags.operationOpen + 'each\\s*(\\w*?)\\s*in\\s*range\\(([^}]+?)\\s*,\\s*([^}]+?)\\)' + juicer.tags.operationClose;
    var include = juicer.tags.operationOpen + 'include\\s*([^}]*?)\\s*,\\s*([^}]*?)' + juicer.tags.operationClose;
    var helperRegisterStart = juicer.tags.operationOpen + 'helper\\s*([^}]*?)\\s*' + juicer.tags.operationClose;
    var helperRegisterBody = '([\\s\\S]*?)';
    var helperRegisterEnd = juicer.tags.operationOpen + '\\/helper' + juicer.tags.operationClose;

    juicer.settings.forstart = new RegExp(forstart, 'igm');
    juicer.settings.forend = new RegExp(forend, 'igm');
    juicer.settings.ifstart = new RegExp(ifstart, 'igm');
    juicer.settings.ifend = new RegExp(ifend, 'igm');
    juicer.settings.elsestart = new RegExp(elsestart, 'igm');
    juicer.settings.elseifstart = new RegExp(elseifstart, 'igm');
    juicer.settings.interpolate = new RegExp(interpolate, 'igm');
    juicer.settings.noneencode = new RegExp(noneencode, 'igm');
    juicer.settings.inlinecomment = new RegExp(inlinecomment, 'igm');
    juicer.settings.rangestart = new RegExp(rangestart, 'igm');
    juicer.settings.include = new RegExp(include, 'igm');
    juicer.settings.helperRegister = new RegExp(helperRegisterStart + helperRegisterBody + helperRegisterEnd, 'igm');
};

juicer.tagInit();

// Using this method to set the options by given conf-name and conf-value,
// you can also provide more than one key-value pair wrapped by an object.
// this interface also used to custom the template tag delimater, for this
// situation, the conf-name must begin with tag::, for example: juicer.set
// ('tag::operationOpen', '{@').

juicer.set = function(conf, value) {
    var that = this;

    var escapePattern = function(v) {
        return v.replace(/[\$\(\)\[\]\+\^\{\}\?\*\|\.]/igm, function($) {
            return '\\' + $;
        });
    };

    var set = function(conf, value) {
        var tag = conf.match(/^tag::(.*)$/i);

        if(tag) {
            that.tags[tag[1]] = escapePattern(value);
            that.tagInit();
            return;
        }

        that.options[conf] = value;
    };

    if(arguments.length === 2) {
        set(conf, value);
        return;
    }

    if(conf === Object(conf)) {
        for(var i in conf) {
            if(conf.hasOwnProperty(i)) {
                set(i, conf[i]);
            }
        }
    }
};

// Before you're using custom functions in your template like ${name | fnName},
// you need to register this fn by juicer.register('fnName', fn).

juicer.register = function(fname, fn) {
    var _method = this.options._method;

    if(_method.hasOwnProperty(fname)) {
        return false;
    }

    return _method[fname] = fn;
};

// remove the registered function in the memory by the provided function name.
// for example: juicer.unregister('fnName').

juicer.unregister = function(fname) {
    var _method = this.options._method;

    if(_method.hasOwnProperty(fname)) {
        return delete _method[fname];
    }
};

juicer.template = function(options) {
    var that = this;

    this.options = options;

    this.__interpolate = function(_name, _escape, options) {
        var _define = _name.split('|'), _fn = _define[0] || '', _cluster;

        if(_define.length > 1) {
            _name = _define.shift();
            _cluster = _define.shift().split(',');
            _fn = '_method.' + _cluster.shift() + '.call(this, ' + [_name].concat(_cluster) + ')';
        }

        return '<%= ' + (_escape ? '_method.__escapehtml.escaping' : '') + '(' +
                    (!options || options.detection !== false ? '_method.__escapehtml.detection' : '') + '(' +
                        _fn +
                    ')' +
                ')' +
            ' %>';
    };

    this.__removeShell = function(tpl, options) {
        var _counter = 0;

        tpl = tpl
            // inline helper register
            .replace(juicer.settings.helperRegister, function($, helperName, fnText) {
                var anno = annotate(fnText);
                var fnArgs = anno[0];
                var fnBody = anno[1];
                var fn = new Function(fnArgs.join(','), fnBody);

                juicer.register(helperName, fn);
                return $;
            })

            // for expression
            .replace(juicer.settings.forstart, function($, _name, alias, key) {
                var alias = alias || 'value', key = key && key.substr(1);
                var _iterate = 'i' + _counter++;
                return '<% ~function() {' +
                            'for(var ' + _iterate + ' in ' + _name + ') {' +
                                'if(' + _name + '.hasOwnProperty(' + _iterate + ')) {' +
                                    'var ' + alias + '=' + _name + '[' + _iterate + '];' +
                                    (key ? ('var ' + key + '=' + _iterate + ';') : '') +
                        ' %>';
            })
            .replace(juicer.settings.forend, '<% }}}(); %>')

            // if expression
            .replace(juicer.settings.ifstart, function($, condition) {
                return '<% if(' + condition + ') { %>';
            })
            .replace(juicer.settings.ifend, '<% } %>')

            // else expression
            .replace(juicer.settings.elsestart, function($) {
                return '<% } else { %>';
            })

            // else if expression
            .replace(juicer.settings.elseifstart, function($, condition) {
                return '<% } else if(' + condition + ') { %>';
            })

            // interpolate without escape
            .replace(juicer.settings.noneencode, function($, _name) {
                return that.__interpolate(_name, false, options);
            })

            // interpolate with escape
            .replace(juicer.settings.interpolate, function($, _name) {
                return that.__interpolate(_name, true, options);
            })

            // clean up comments
            .replace(juicer.settings.inlinecomment, '')

            // range expression
            .replace(juicer.settings.rangestart, function($, _name, start, end) {
                var _iterate = 'j' + _counter++;
                return '<% ~function() {' +
                            'for(var ' + _iterate + '=' + start + ';' + _iterate + '<' + end + ';' + _iterate + '++) {{' +
                                'var ' + _name + '=' + _iterate + ';' +
                        ' %>';
            })

            // include sub-template
            .replace(juicer.settings.include, function($, tpl, data) {
                // compatible for node.js
                if(tpl.match(/^file\:\/\//igm)) return $;
                return '<%= _method.__juicer(' + tpl + ', ' + data + '); %>';
            });

        // exception handling
        if(!options || options.errorhandling !== false) {
            tpl = '<% try { %>' + tpl;
            tpl += '<% } catch(e) {_method.__throw("Juicer Render Exception: "+e.message);} %>';
        }

        return tpl;
    };

    this.__toNative = function(tpl, options) {
        return this.__convert(tpl, !options || options.strip);
    };

    this.__lexicalAnalyze = function(tpl) {
        var buffer = [];
        var method = [];
        var prefix = '';
        var reserved = [
            'if', 'each', '_', '_method', 'console',
            'break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete', 'do',
            'finally', 'for', 'function', 'in', 'instanceof', 'new', 'return', 'switch',
            'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with', 'null', 'typeof',
            'class', 'enum', 'export', 'extends', 'import', 'super', 'implements', 'interface',
            'let', 'package', 'private', 'protected', 'public', 'static', 'yield', 'const', 'arguments',
            'true', 'false', 'undefined', 'NaN'
        ];

        var indexOf = function(array, item) {
            if (Array.prototype.indexOf && array.indexOf === Array.prototype.indexOf) {
                return array.indexOf(item);
            }

            for(var i=0; i < array.length; i++) {
                if(array[i] === item) return i;
            }

            return -1;
        };

        var variableAnalyze = function($, statement) {
            statement = statement.match(/\w+/igm)[0];

            if(indexOf(buffer, statement) === -1 && indexOf(reserved, statement) === -1 && indexOf(method, statement) === -1) {

                // avoid re-declare native function, if not do this, template
                // `{@if encodeURIComponent(name)}` could be throw undefined.

                if(typeof(window) !== 'undefined' && typeof(window[statement]) === 'function' && window[statement].toString().match(/^\s*?function \w+\(\) \{\s*?\[native code\]\s*?\}\s*?$/i)) {
                    return $;
                }

                // avoid re-declare registered function, if not do this, template
                // `{@if registered_func(name)}` could be throw undefined.

                if(typeof(juicer.options._method[statement]) === 'function' || juicer.options._method.hasOwnProperty(statement)) {
                    method.push(statement);
                    return $;
                }

                // avoid SyntaxError: Unexpected number

                if(statement.match(/^\d+/igm)) {
                    return $;
                }

                buffer.push(statement); // fuck ie
            }

            return $;
        };

        tpl.replace(juicer.settings.forstart, variableAnalyze).
            replace(juicer.settings.interpolate, variableAnalyze).
            replace(juicer.settings.ifstart, variableAnalyze).
            replace(juicer.settings.elseifstart, variableAnalyze).
            replace(juicer.settings.include, variableAnalyze).
            replace(/[\+\-\*\/%!\?\|\^&~<>=,\(\)\[\]]\s*([A-Za-z_0-9]+)/igm, variableAnalyze);

        for(var i = 0; i < buffer.length; i++) {
            prefix += 'var ' + buffer[i] + '=_.' + buffer[i] + ';';
        }

        for(var i = 0; i < method.length; i++) {
            prefix += 'var ' + method[i] + '=_method.' + method[i] + ';';
        }

        return '<% ' + prefix + ' %>';
    };

    this.__convert=function(tpl, strip) {
        var buffer = [].join('');

        buffer += "'use strict';"; // use strict mode
        buffer += "var _=_||{};";
        buffer += "var _out='';_out+='";

        if(strip !== false) {
            buffer += tpl
                .replace(/\\/g, "\\\\")
                .replace(/[\r\t\n]/g, " ")
                .replace(/'(?=[^%]*%>)/g, "\t")
                .split("'").join("\\'")
                .split("\t").join("'")
                .replace(/<%=(.+?)%>/g, "';_out+=$1;_out+='")
                .split("<%").join("';")
                .split("%>").join("_out+='")+
                "';return _out;";

            return buffer;
        }

        buffer += tpl
                .replace(/\\/g, "\\\\")
                .replace(/[\r]/g, "\\r")
                .replace(/[\t]/g, "\\t")
                .replace(/[\n]/g, "\\n")
                .replace(/'(?=[^%]*%>)/g, "\t")
                .split("'").join("\\'")
                .split("\t").join("'")
                .replace(/<%=(.+?)%>/g, "';_out+=$1;_out+='")
                .split("<%").join("';")
                .split("%>").join("_out+='")+
                "';return _out.replace(/[\\r\\n]\\s+[\\r\\n]/g, '\\r\\n');";

        return buffer;
    };

    this.parse = function(tpl, options) {
        var _that = this;

        if(!options || options.loose !== false) {
            tpl = this.__lexicalAnalyze(tpl) + tpl;
        }

        tpl = this.__removeShell(tpl, options);
        tpl = this.__toNative(tpl, options);

        this._render = new Function('_, _method', tpl);

        this.render = function(_, _method) {
            if(!_method || _method !== that.options._method) {
                _method = __creator(_method, that.options._method);
            }

            return _that._render.call(this, _, _method);
        };

        return this;
    };
};

juicer.compile = function(tpl, options) {
    if(!options || options !== this.options) {
        options = __creator(options, this.options);
    }

    var that = this;
    var cacheStore = {
        get: function(tpl) {
            if(options.cachestore) {
                return options.cachestore.get(tpl);
            }

            return that.__cache[tpl];
        },

        set: function(tpl, val) {
            if(options.cachestore) {
                return options.cachestore.set(tpl, val);
            }

            return that.__cache[tpl] = val;
        }
    };

    try {
        var engine = cacheStore.get(tpl) ?
            cacheStore.get(tpl) :
            new this.template(this.options).parse(tpl, options);

        if(!options || options.cache !== false) {
            cacheStore.set(tpl, engine);
        }

        return engine;

    } catch(e) {
        __throw('Juicer Compile Exception: ' + e.message);

        return {
            render: function() {} // noop
        };
    }
};

juicer.to_html = function(tpl, data, options) {
    if(!options || options !== this.options) {
        options = __creator(options, this.options);
    }

    return this.compile(tpl, options).render(data, options._method);
};

if(typeof(document) !== 'undefined' && document.body) {
    juicer.documentHTML = document.body.innerHTML;
}

//add by bh-lay
export default juicer;